output
函數傳回一個 OutputEmitterRef
,其他使用者可以訂閱該值來操作該值。 除了 subscribe
之外,OutputEmitterRef
還可以使用 outputToObservable
函數轉換為 Observable
。此函數與 toSignal
和 toObservable
一起位於 rxjs-interop
套件中。
一種用例是組件向父組件發出一個值,父組件使用該值發出 HTTP 請求以從伺服器檢索資料。
我們重構 Star War 應用程式來看看它是如何完成的。 當使用者選擇一個角色並點擊按鈕時,該組件會將 id 傳送到 App
組件。 App
組件呼叫後端來檢索 Star War 角色並將資料指派給 StarWarCharacterComponent
組件的 input
。
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
export const appConfig = {
providers: [
provideHttpClient(),
provideExperimentalZonelessChangeDetection()
]
}
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app.config';
bootstrapApplication(App, appConfig);
提供 Http client
和 zoneless
功能,並引導應用程式設定。
import { catchError, map, of } from 'rxjs';
import { inject, runInInjectionContext, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export type Person = {
name: string;
height: string;
mass: string;
hair_color: string;
skin_color: string;
eye_color: string;
gender: string;
films: string[];
}
const URL = 'https://swapi.dev/api/people';
export function getPerson(id: number, injector: Injector) {
return runInInjectionContext(injector, () => {
const http = inject(HttpClient);
return http.get<Person>(`${URL}/${id}`).pipe(
map((p) => ({ ...p, id })),
catchError((err) => {
console.error(err);
return of(undefined);
}));
});
}
getPerson
函數透過 id 檢索星際大戰角色。
import { Person } from "./star-war.api";
export type ListItem = {
id: number;
name: string;
}
export type PersonWithId = Person & { id: number };
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { PersonWithId } from './star-war.type';
@Component({
selector: 'app-star-war-character',
standalone: true,
imports: [FormsModule],
template: `
<div class="border">
@if(person(); as person) {
<p>Id: {{ person.id }} </p>
<p>Name: {{ person.name }}</p>
<p>Height: {{ person.height }}</p>
<p>Mass: {{ person.mass }}</p>
<p>Hair Color: {{ person.hair_color }}</p>
<p>Skin Color: {{ person.skin_color }}</p>
<p>Eye Color: {{ person.eye_color }}</p>
<p>Gender: {{ person.gender }}</p>
<button (click)=”clone.emit(person.id)>Clone me</button>
} @else {
<p>No info</p>
}
</div>
`,
})
export class AppStarWarCharacterComponent {
person = input<undefined | PersonWithId>(undefined);
clone = output<number>();
}
AppStarWarCharacterComponent
有一個 person
input,顯示人物的詳細資料。
import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ListItem } from './star-war.type';
@Component({
selector: 'app-star-war-list',
standalone: true,
imports: [FormsModule],
template: `
<select [(ngModel)]="jediId">
<option value="0">---select---</option>
@for (p of persons(); track p.id) {
<option [value]="p.id">{{ p.name }}</option>
}
</select>
<button (click)="id.emit(jediId())" [disabled]="jediId() === 0">Add a character</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StarWarListComponent {
persons = input<ListItem[]>([]);
jediId = signal(0);
id = output<number>();
}
當使用者從下拉清單中選擇一個值並點擊按鈕時,id
output 會將該值傳送到父組件。
import { ChangeDetectionStrategy, Component, ComponentRef, effect, inject, Injector, OnDestroy, signal, viewChild, ViewContainerRef } from '@angular/core';
import { AppStarWarListComponent } from './star-war/star-war-list.component';
import { ListItem, PersonWithId } from './star-war/star-war.type';
import { outputToObservable } from '@angular/core/rxjs-interop';
import { getPerson } from './star-war/star-war.api';
import { switchMap } from 'rxjs';
@Component({
selector: 'app-root',
standalone: true,
imports: [AppStarWarListComponent],
template: `
<div class="container">
<ng-container #vcr />
</div>
<app-star-war-list [persons]="jedis()" />
`,
})
export class App implements OnDestroy {
jedis = signal([
{ id: 1, name: 'Luke' },
{ id: 10, name: 'Obi Wan Kenobe' },
{ id: 20, name: 'Yoda' },
] as ListItem[]);
list = viewChild.required(StarWarListComponent);
injector = inject(Injector);
vcr = viewChild.required('vcr', { read: ViewContainerRef });
componentRefs = [] as ComponentRef<any>[];
constructor() {
effect((OnCleanUp) => {
const sub = outputToObservable(this.list().id)
.pipe(switchMap((id) => getPerson(id, this.injector)))
.subscribe((person) => this.addAJedi(person));
OnCleanUp(() => sub.unsubscribe());
});
}
async addAJedi(person: PersonWithId | undefined) {
const { AppStarWarCharacterComponent } = await import ('./star-war/star-war-character.component');
AppStarWarCharacterComponent
const componentRef = this.vcr().createComponent(AppStarWarCharacterComponent);
componentRef.setInput('person', person);
this.componentRefs.push(componentRef);
}
ngOnDestroy(): void {
if (this.componentRefs) {
for (const ref of this.componentRefs) {
ref.destroy();
}
}
}
}
list = viewChild.required(AppStarWarListComponent);
App
組件使用 viewChild
函數來查詢 AppStarWarListComponent
組件。
constructor() {
effect((OnCleanUp) => {
const sub = outputToObservable(this.list().id)
.pipe(
switchMap((id) => getPerson(id, this.injector))
)
.subscribe((person) => this.addAJedi(person));
OnCleanUp(() => sub.unsubscribe());
});
}
this.list().id
input signal 是 effect
的依賴項 (dependency),outputToObservable
函數將其轉換為 Observable
。 當使用者從下拉清單中選擇一個值時,效果將執行邏輯。 Observable
將 id
傳送給 switchMap
運算子來呼叫 Star War API 並訂閱 Observable
以獲得結果。 回呼函數 (callback) 會動態匯入 AppStarWarCharacterComponent
組件,將結果指派給 input signal,並將組件附加到 ViewContainerRef
。 subscribe
方法建立一個訂閱 (subscription);因此,OnCleanUp
回呼 (callback) 會在 effect
銷毀之前取消訂閱 (subscription)。
ngOnDestroy(): void {
if (this.componentRefs) {
for (const ref of this.componentRefs) {
ref.destroy();
}
}
}
當應用程式銷毀 App
組件時,ngOnDestroy
會釋放 componentRefs
的記憶體以避免 memory leaks。
<button (click)="clone.emit(person.id)">Clone me</button>
export class AppStarWarCharacterComponent {
person = input<undefined | PersonWithId>(undefined);
clone = output<number>();
}
AppStarWarCharacterComponent
中有一個 Clone me
按鈕可以複製自身。點選按鈕時,clone
output會將 id
傳送到 App
組件。
destroyRef = inject(DestroyRef);
async addAJedi(person: PersonWithId | undefined) {
const { AppStarWarCharacterComponent } = await import ('./star-war/star-war-character.component');
const componentRef = this.vcr().createComponent(AppStarWarCharacterComponent);
componentRef.setInput('person', person);
outputToObservable(componentRef.instance.clone)
.pipe(
switchMap((id) => getPerson(id, this.injector)),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((aClone) => this.addAJedi(aClone));
this.componentRefs.push(componentRef);
}
outputToObservable
函數將 componentRef.instance.clone OutputRef
轉換為 Observable
以發出新的 HTTP 請求,並將結果附加到 ViewContainerRef
。 takeUntilDestroyed
運算子使用DestroyRef
在 App
被銷毀時完成 Observable
。
outputToObservable
將 output
轉換為 Observable
,以便該值可以傳送給 RxJS 運算符outputToObservable
駐留在 rxjs-interop
套件中,就像 toSignal
和 toObservable
一樣outputToObservable
創建 Observable
並訂閱 (subscribe) 它時,我們需要取消訂閱 (subscription)。鐵人賽的第 24 天到此結束。